Ths labs will cover how to user the Game Caretaker in our Unity SDK in order to add undo/redo support to a new or existing game.

The Gameboard undo/redo support is centered around the following elements in our SDK:

In the following sections we'll go through a sample and put this all together to do/undo/redo actions in our game.

In our game we'll have a simple player with two properties: name, and hitpoints. Because we want to be able to undo/redo changes to this player's state over time we'll make this player an Originator by implementing the IOriginator interface.

    using Gameboard.Persistance;

    public class Player : IOriginator
    {

        private string mName;
        private int mHitpoints;

        public Player(string name, int hitpoints)
        {
            mName = name;
            mHitpoints = hitpoints;
        }

        public IMemento CreateMemento()
        {
            return new PlayerMemento(mName, mHitpoints);
        }

        public void SetMemento(IMemento memento)
        {
            if (!(memento is PlayerMemento))
            {
                return;
            }

            PlayerMemento playerMemento = memento as PlayerMemento;

            mHitpoints = playerMemento.GetHitpoints();
            mName = playerMemento.GetName();

        }

        public void Clear()
        {
            // Handle any clear needed.
        }

    }

At this time the player does not really do much so let's add some functions to be able to change this player's state.

    public void AddHitpoints(int delta)
    {
        mHitpoints += delta;
    }

    public void SubtractHitpoints(int delta)
    {
        mHitpoints -= delta;
    }

    public string GetName()
    {
        return mName;
    }

    public int GetHitpoints()
    {
        return mHitpoints;
    }

    public void UpdateName(string name)
    {
        mName = name;
    }

Now that we have our player we need to implement a Memento that will let us save and restore this player's state over time. Our memento will track the state of our player which in our example is a function of player name and player hitpoints. In order to support this, we'll make sure our Player Memento implements the IMemento interface.

    private class PlayerMemento: IMemento
    {
        private int mHitpoints;
        private string mName;

        public PlayerMemento(string name, int hitpoints)
        {
            mHitpoints = hitpoints;
            mName = name;
        }

        public int GetHitpoints()
        {
            return mHitpoints;
        }

        public string GetName()
        {
            return mName;
        }
    }

Finally, Because the memento should be opaque to other objects we'll make sure to define it as an inner class of the originator that creates it. So together the entire player will look like this:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    using Gameboard.Persistance;

    public class Player : IOriginator
    {

        private string mName;
        private int mHitpoints;

        public Player(string name, int hitpoints)
        {
            mName = name;
            mHitpoints = hitpoints;
        }

        public void AddHitpoints(int delta)
        {
            mHitpoints += delta;
        }

        public void SubtractHitpoints(int delta)
        {
            mHitpoints -= delta;
        }

        public string GetName()
        {
            return mName;
        }

        public int GetHitpoints()
        {
            return mHitpoints;
        }

        public void UpdateName(string name)
        {
            mName = name;
        }

        public IMemento CreateMemento()
        {
            return new PlayerMemento(mName, mHitpoints);
        }

        public void SetMemento(IMemento memento)
        {
            if (!(memento is PlayerMemento))
            {
                return;
            }

            PlayerMemento playerMemento = memento as PlayerMemento;

            mHitpoints = playerMemento.GetHitpoints();
            mName = playerMemento.GetName();

        }

        public void Clear()
        {
            // Handle any clear needed.
        }

        private class PlayerMemento: IMemento
        {
            private int mHitpoints;
            private string mName;

            public PlayerMemento(string name, int hitpoints)
            {
                mHitpoints = hitpoints;
                mName = name;
            }

            public int GetHitpoints()
            {
                return mHitpoints;
            }

            public string GetName()
            {
                return mName;
            }
        }

    }

In our game we want the player to be able to cast two spells: heal and harm. Because these actions cause a change on the player state which we want to be able to undo/redo in case players change their mind about an action will implement them as commands by ensuring they extend the ACommand class.

Note: Commands know how to perform a given action as well as what steps to perform to reverse of such an action in case we want to perform specific feedback to our players during each event.

First we'll implement our heal spells which has a target player and the ammount of HP we want to heal.

using Gameboard.Persistance;

public class HealCommand : ACommand
{

    private Player mPlayer;
    private int mAmount;

    public HealCommand(Player target, int amount)
    {
        mPlayer = target;
        mAmount = amount;
    }

    override
    public void ExecuteImpl()
    {
        mPlayer.AddHitpoints(mAmount);
    }

    override
    public void UnExecuteImpl()
    {
        mPlayer.SubtractHitpoints(mAmount);
    }
}

In the case of our heal spell we want to add a certain amount of HP to the target user. So in our ExecuteImpl() we'll update that user's HP.

    override
    public void ExecuteImpl()
    {
        mPlayer.AddHitpoints(mAmount);
    }

While we also want to take away the same amount if we change our mind and want to take back the action. So in our UnExecuteImpl() we'll take back those hitpoints.

    override
    public void UnExecuteImpl()
    {
        mPlayer.SubtractHitpoints(mAmount);
    }

Note: You player state is recorded during each execute/unexecute so if you miss your UnExecuteImpl() implementation the player state is still retored correctly.

Our harm spell will follow a similar pattern but take HP from the player instead:

    using Gameboard.Persistance;

    public class HarmCommand : ACommand
    {

        private Player mPlayer;
        private int mAmount;

        public HarmCommand(Player target, int amount)
        {
            mPlayer = target;
            mAmount = amount;
        }

        override
        public void ExecuteImpl()
        {
            mPlayer.SubtractHitpoints(mAmount);
        }

        override
        public void UnExecuteImpl()
        {
            mPlayer.AddHitpoints(mAmount);
        }
    }

With our player and spells defined we can now develop our MonoBehavior in Unity. In order to ensure that our Player is tracked and handled by the Game Caretaker in our SDK we'll make sure to register our player with it either on Awake or on Start. In this example we'll do on on Awake:

    void Awake()
    {
        mPlayer = new Player(PlayerName, Hitpoints);
        // Register the players as a stateful element to be tracked.
        GameCaretaker.GetInstance().RegisterOriginator(mPlayer);
    }

And we'll define a function to be called from the game that will heal our player:

    public void HealPlayer()
    {
        HealCommand command = new HealCommand(mPlayer, 10);
        // We never execute the command directly but instead request the execution of the command.
        GameCaretaker.GetInstance().Execute(command);

    }

And one that will harm our player:

    public void HarmPlayer()
    {
        HarmCommand command = new HarmCommand(mPlayer, 10);
        // We never execute the command directly but instead request the execution of the command.
        GameCaretaker.GetInstance().Execute(command);
    }

As you have probably noticed by now our GameCaretaker handles all of the work for us so we don't have to trouble ourselves with the specifics.

Putting all together our player behavior looks likes:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    using TMPro;
    using Gameboard.Persistance;

    public class PlayerBehavior : MonoBehaviour
    {

        public string PlayerName;
        public int Hitpoints;
        public TMP_Text LabelText;

        private Player mPlayer;

        void Awake()
        {
            mPlayer = new Player(PlayerName, Hitpoints);
            // Register the players as a stateful element to be tracked.
            GameCaretaker.GetInstance().RegisterOriginator(mPlayer);
        }
        // Start is called before the first frame update
        void Start()
        {

        }

        // Update is called once per frame
        void Update()
        {
            LabelText.text = mPlayer.GetName() + ": " + mPlayer.GetHitpoints();
        }

        public void HarmPlayer()
        {
            HarmCommand command = new HarmCommand(mPlayer, 10);
            // We never execute the command directly but instead request the execution of the command.
            GameCaretaker.GetInstance().Execute(command);
        }

        public void HealPlayer()
        {
            HealCommand command = new HealCommand(mPlayer, 10);
            // We never execute the command directly but instead request the execution of the command.
            GameCaretaker.GetInstance().Execute(command);

        }
    }

With our player ready and registered with the game caretaker we can move on and have our game support undo/redo. We'll go ahead and define a MonoBehavior to expose these features as buttons on the UI when the undo/redo are available.

As you may have guessed by now these features are all handled through the GameCaretaker, so our undo method looks like this:

    public void Undo()
    {
        GameCaretaker.GetInstance().Undo();
    }

And our redo method will look like this:

    public void Redo()
    {
        GameCaretaker.GetInstance().Redo();
    }

We'll also want to only show the undo and redo buttons when the player can actually perform them so during our Update() we'll set their active state based on whether or not each feature is available.

    // Update is called once per frame
    void Update()
    {
        UndoButton.SetActive(GameCaretaker.GetInstance().CanUndo());
        RedoButton.SetActive(GameCaretaker.GetInstance().CanRedo());
    }

Finally we also want to allow the player to start a new round so we'll expand out controller by adding a NewGame() function:

    public void NewGame()
    {
        GameCaretaker.GetInstance().Clear();
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }

Putting all together our MonoBehavior looks like so:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;

    using TMPro;
    using Gameboard.Persistance;


    public class RedoController : MonoBehaviour
    {
        public GameObject UndoButton;
        public GameObject RedoButton;
        // Start is called before the first frame update
        void Start()
        {

        }

        // Update is called once per frame
        void Update()
        {
            UndoButton.SetActive(GameCaretaker.GetInstance().CanUndo());
            RedoButton.SetActive(GameCaretaker.GetInstance().CanRedo());
        }

        public void Undo()
        {
            GameCaretaker.GetInstance().Undo();
        }

        public void Redo()
        {
            GameCaretaker.GetInstance().Redo();
        }

        public void NewGame()
        {
            GameCaretaker.GetInstance().Clear();
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        }
    }

This section include the entire code in one single, easy to copy section.

Player.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    using Gameboard.Persistance;

    public class Player : IOriginator
    {

        private string mName;
        private int mHitpoints;

        public Player(string name, int hitpoints)
        {
            mName = name;
            mHitpoints = hitpoints;
        }

        public void AddHitpoints(int delta)
        {
            mHitpoints += delta;
        }

        public void SubtractHitpoints(int delta)
        {
            mHitpoints -= delta;
        }

        public string GetName()
        {
            return mName;
        }

        public int GetHitpoints()
        {
            return mHitpoints;
        }

        public void UpdateName(string name)
        {
            mName = name;
        }

        public IMemento CreateMemento()
        {
            return new PlayerMemento(mName, mHitpoints);
        }

        public void SetMemento(IMemento memento)
        {
            if (!(memento is PlayerMemento))
            {
                return;
            }

            PlayerMemento playerMemento = memento as PlayerMemento;

            mHitpoints = playerMemento.GetHitpoints();
            mName = playerMemento.GetName();

        }

        public void Clear()
        {
            // Handle any clear needed.
        }

        private class PlayerMemento: IMemento
        {
            private int mHitpoints;
            private string mName;

            public PlayerMemento(string name, int hitpoints)
            {
                mHitpoints = hitpoints;
                mName = name;
            }

            public int GetHitpoints()
            {
                return mHitpoints;
            }

            public string GetName()
            {
                return mName;
            }
        }

    }

HealCommand.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    using Gameboard.Persistance;

    public class HealCommand : ACommand
    {

        private Player mPlayer;
        private int mAmount;

        public HealCommand(Player target, int amount)
        {
            mPlayer = target;
            mAmount = amount;
        }

        override
        public void ExecuteImpl()
        {
            mPlayer.AddHitpoints(mAmount);
        }

        override
        public void UnExecuteImpl()
        {
            mPlayer.SubtractHitpoints(mAmount);
        }
    }

HarmCommand.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    using Gameboard.Persistance;

    public class HarmCommand : ACommand
    {

        private Player mPlayer;
        private int mAmount;

        public HarmCommand(Player target, int amount)
        {
            mPlayer = target;
            mAmount = amount;
        }

        override
        public void ExecuteImpl()
        {
            mPlayer.SubtractHitpoints(mAmount);
        }

        override
        public void UnExecuteImpl()
        {
            mPlayer.AddHitpoints(mAmount);
        }
    }

PlayerBehavior.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    using TMPro;
    using Gameboard.Persistance;

    public class PlayerBehavior : MonoBehaviour
    {

        public string PlayerName;
        public int Hitpoints;
        public TMP_Text LabelText;

        private Player mPlayer;

        void Awake()
        {
            mPlayer = new Player(PlayerName, Hitpoints);
            // Register the players as a stateful element to be tracked.
            GameCaretaker.GetInstance().RegisterOriginator(mPlayer);
        }
        // Start is called before the first frame update
        void Start()
        {

        }

        // Update is called once per frame
        void Update()
        {
            LabelText.text = mPlayer.GetName() + ": " + mPlayer.GetHitpoints();
        }

        public void HarmPlayer()
        {
            HarmCommand command = new HarmCommand(mPlayer, 10);
            // We never execute the command directly but instead request the execution of the command.
            GameCaretaker.GetInstance().Execute(command);
        }

        public void HealPlayer()
        {
            HealCommand command = new HealCommand(mPlayer, 10);
            // We never execute the command directly but instead request the execution of the command.
            GameCaretaker.GetInstance().Execute(command);

        }
    }

RedoController.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;

    using TMPro;
    using Gameboard.Persistance;


    public class RedoController : MonoBehaviour
    {
        public GameObject UndoButton;
        public GameObject RedoButton;
        // Start is called before the first frame update
        void Start()
        {

        }

        // Update is called once per frame
        void Update()
        {
            UndoButton.SetActive(GameCaretaker.GetInstance().CanUndo());
            RedoButton.SetActive(GameCaretaker.GetInstance().CanRedo());
        }

        public void Undo()
        {
            GameCaretaker.GetInstance().Undo();
        }

        public void Redo()
        {
            GameCaretaker.GetInstance().Redo();
        }

        public void NewGame()
        {
            GameCaretaker.GetInstance().Clear();
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        }
    }